﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using devtm.Aop;
using devtm.Aop.Extensions;
using devtm.Aop.Convert;
using Mono.Cecil;
using System.Reflection;
using System.IO;
using devtm.Aop.Copy;
using devtm.Aop.Emit;
using Mono.Cecil.Cil;
using devtm.Aop.Linq;


namespace System.ServiceModel.Dynamic
{

    public static class WcfWrapperFactory
    {


        #region Ctor

        #region Initialize

        public static Type Initialize<T>()
        {
            Type t = Initialize<T>(null);
            return t;
        }

        public static Type Initialize<T>(ProxyBuild<T> proxy)
        {
            Type t = GetWrapperFor(typeof(T), proxy, Script);
            return t;
        }

        #endregion

        #region Create

        /// <summary>Initializes a new instance of the <see cref="T:System.ServiceModel.Client`1" /> class using the default target endpoint from the application configuration file. </summary>
        /// <exception cref="T:System.InvalidOperationException">Either there is no default endpoint information in the configuration file, more than one endpoint in the file, or no configuration file.</exception>
        public static T Create<T>(params IEndpointBehavior[] behaviors) 
        {
            Type type = Initialize<T>(null);
            T retour = (T)Activator.CreateInstance(type, new object[] { behaviors });
            return retour;
        }

        /// <summary>Initializes a new instance of the <see cref="T:System.ServiceModel.Client`1" /> class using the configuration information specified in the application configuration file by <paramref name="endpointConfigurationName" />. </summary>
        /// <param name="endpointConfigurationName">The name of the endpoint in the application configuration file.</param>
        /// <exception cref="T:System.ArgumentNullException">The specified endpoint information is null.</exception>
        /// <exception cref="T:System.InvalidOperationException">The endpoint cannot be found or the endpoint contract is not valid.</exception>
        public static T Create<T>(string endpointConfigurationName, params IEndpointBehavior[] behaviors)
        {
            Type type = Initialize<T>(null);
            T retour = (T)Activator.CreateInstance(type, new object[] { endpointConfigurationName, behaviors });
            return retour;
        }

        /// <summary>Initializes a new instance of the <see cref="T:System.ServiceModel.Client`1" /> class.</summary>
        /// <param name="endpointConfigurationName">The name of the endpoint in the application configuration file.</param>
        /// <param name="remoteAddress">The address of the service.</param>
        /// <exception cref="T:System.ArgumentNullException">The endpoint is null.</exception>
        /// <exception cref="T:System.ArgumentNullException">The remote address is null.</exception>
        /// <exception cref="T:System.InvalidOperationException">The endpoint cannot be found or the endpoint contract is not valid.</exception>
        public static T Create<T>(string endpointConfigurationName, string remoteAddress, params IEndpointBehavior[] behaviors)
        {
            Type type = Initialize<T>(null);
            T retour = (T)Activator.CreateInstance(type, new object[] { endpointConfigurationName, remoteAddress, behaviors });
            return retour;
        }

        /// <summary>Initializes a new instance of the <see cref="T:System.ServiceModel.Client`1" /> class using the specified target address and endpoint information. </summary>
        /// <param name="endpointConfigurationName">The name of the endpoint in the application configuration file.</param>
        /// <param name="remoteAddress">The address of the service.</param>
        /// <exception cref="T:System.ArgumentNullException">The endpoint is null.</exception>
        /// <exception cref="T:System.ArgumentNullException">The remote address is null.</exception>
        /// <exception cref="T:System.InvalidOperationException">The endpoint cannot be found or the endpoint contract is not valid.</exception>
        public static T Create<T>(string endpointConfigurationName, EndpointAddress remoteAddress, params IEndpointBehavior[] behaviors)
        {
            Type type = Initialize<T>(null);
            T retour = (T)Activator.CreateInstance(type, new object[] { endpointConfigurationName, remoteAddress, behaviors });
            return retour;
        }

        /// <summary>Initializes a new instance of the <see cref="T:System.ServiceModel.Client`1" /> class using the specified binding and target address. </summary>
        /// <param name="binding">The binding with which to make calls to the service.</param>
        /// <param name="remoteAddress">The address of the service endpoint.</param>
        /// <exception cref="T:System.ArgumentNullException">The binding is null.</exception>
        /// <exception cref="T:System.ArgumentNullException">The remote address is null.</exception>
        public static T Create<T>(Binding binding, EndpointAddress remoteAddress, params IEndpointBehavior[] behaviors)
        {
            Type type = Initialize<T>(null);
            T retour = (T)Activator.CreateInstance(type, new object[] { binding, remoteAddress });
            return retour;
        }

        /// <summary>Initializes a new instance of the <see cref="T:System.ServiceModel.Client`1" /> class using the specified  <see cref="T:System.ServiceModel.Description.ServiceEndpoint" />.</summary>
        /// <param name="endpoint">The endpoint for a service that allows clients to find and communicate with the service.</param>
        public static T Create<T>(ServiceEndpoint endpoint, params IEndpointBehavior[] behaviors)
        {
            Type type = Initialize<T>(null);
            T retour = (T)Activator.CreateInstance(type, new object[] { endpoint, behaviors });
            return retour;
        }

        /// <summary>Initializes a new instance of the <see cref="T:System.ServiceModel.Client`1" /> class using the <paramref name="callbackInstance" /> as the callback object in a duplex conversation. </summary>
        /// <param name="callbackInstance">The callback object that the client application uses to listen for messages from the connected service.</param>
        /// <exception cref="T:System.ArgumentNullException">The callback instance is null.</exception>
        /// <exception cref="T:System.InvalidOperationException">Either there is no default endpoint information in the configuration file, more than one endpoint in the file, or no configuration file.</exception>
        public static T Create<T>(InstanceContext callbackInstance, params IEndpointBehavior[] behaviors) 
        {
            Type type = Initialize<T>(null);
            T retour = (T)Activator.CreateInstance(type, new object[] { callbackInstance, behaviors });
            return retour;
        }

        /// <summary>Initializes a new instance of the <see cref="T:System.ServiceModel.Client`1" /> class using the specified callback service and endpoint configuration information. </summary>
        /// <param name="callbackInstance">The callback object that the client uses to listen for messages from the connected service.</param>
        /// <param name="endpointConfigurationName">The name of the endpoint in the application configuration file. </param>
        /// <exception cref="T:System.ArgumentNullException">The callback instance is null.</exception>
        /// <exception cref="T:System.ArgumentNullException">The endpoint is null.</exception>
        /// <exception cref="T:System.InvalidOperationException">The endpoint cannot be found or the endpoint contract is not valid.</exception>
        public static T Create<T>(InstanceContext callbackInstance, string endpointConfigurationName, params IEndpointBehavior[] behaviors)
        {
            Type type = Initialize<T>(null);
            T retour = (T)Activator.CreateInstance(type, new object[] { callbackInstance, endpointConfigurationName, behaviors });
            
            return retour;
        }

        /// <summary>Initializes a new instance of the <see cref="T:System.ServiceModel.Client`1" /> class.</summary>
        /// <param name="callbackInstance">The callback object that the client uses to listen for messages from the connected service.</param>
        /// <param name="endpointConfigurationName">The name of the endpoint in the application configuration file.</param>
        /// <param name="remoteAddress">The address of the service.</param>
        /// <exception cref="T:System.ArgumentNullException">The callback instance is null.</exception>
        /// <exception cref="T:System.ArgumentNullException">The endpoint is null.</exception>
        /// <exception cref="T:System.ArgumentNullException">The remote address is null.</exception>
        /// <exception cref="T:System.InvalidOperationException">The endpoint cannot be found or the endpoint contract is not valid.</exception>
        public static T Create<T>(InstanceContext callbackInstance, string endpointConfigurationName, string remoteAddress, params IEndpointBehavior[] behaviors)
        {
            Type type = Initialize<T>(null);
            T retour = (T)Activator.CreateInstance(type, new object[] { callbackInstance, endpointConfigurationName, remoteAddress, behaviors });
            return retour;
        }

        /// <summary>Initializes a new instance of the <see cref="T:System.ServiceModel.Client`1" /> class.</summary>
        /// <param name="callbackInstance">The callback object that the client uses to listen for messages from the connected service.</param>
        /// <param name="endpointConfigurationName">The name of the endpoint in the application configuration file.</param>
        /// <param name="remoteAddress">The address of the service.</param>
        /// <exception cref="T:System.ArgumentNullException">The callback instance is null.</exception>
        /// <exception cref="T:System.ArgumentNullException">The endpoint is null.</exception>
        /// <exception cref="T:System.ArgumentNullException">The remote address is null.</exception>
        /// <exception cref="T:System.InvalidOperationException">The endpoint cannot be found or the endpoint contract is not valid.</exception>
        public static T Create<T>(InstanceContext callbackInstance, string endpointConfigurationName, EndpointAddress remoteAddress, params IEndpointBehavior[] behaviors)
        {
            Type type = Initialize<T>(null);
            T retour = (T)Activator.CreateInstance(type, new object[] { callbackInstance, endpointConfigurationName, remoteAddress, behaviors });
            return retour;
        }

        /// <summary>Initializes a new instance of the <see cref="T:System.ServiceModel.Client`1" /> class. </summary>
        /// <param name="callbackInstance">The callback service.</param>
        /// <param name="binding">The binding with which to call the service.</param>
        /// <param name="remoteAddress">The address of the service endpoint.</param>
        /// <exception cref="T:System.ArgumentNullException">The callback instance is null.</exception>
        /// <exception cref="T:System.ArgumentNullException">The binding is null.</exception>
        /// <exception cref="T:System.ArgumentNullException">The remote address is null.</exception>
        public static T Create<T>(InstanceContext callbackInstance, Binding binding, EndpointAddress remoteAddress, params IEndpointBehavior[] behaviors)
        {
            Type type = Initialize<T>(null);
            T retour = (T)Activator.CreateInstance(type, new object[] { callbackInstance, binding, remoteAddress, behaviors });
            return retour;
        }

        /// <summary>Initializes a new instance of the <see cref="T:System.ServiceModel.Client`1" /> class using the specified  <see cref="T:System.ServiceModel.InstanceContext" /> and  <see cref="T:System.ServiceModel.Description.ServiceEndpoint" /> objects.</summary>
        /// <param name="callbackInstance">The callback object that the client application uses to listen for messages from the connected service.</param>
        /// <param name="endpoint">The endpoint for a service that allows clients to find and communicate with the service.</param>
        public static T Create<T>(InstanceContext callbackInstance, ServiceEndpoint endpoint, params IEndpointBehavior[] behaviors)
        {
            Type type = Initialize<T>(null);
            T retour = (T)Activator.CreateInstance(type, new object[] { callbackInstance, endpoint, behaviors });
            return retour;
        }

        #endregion

        #endregion


        #region private


        private static Dictionary<Type, Type> _dic = new Dictionary<Type, Type>();
        private static readonly object _lock = new object();
        private static Type GetWrapperFor(Type type, Proxy proxy, bool script)
        {

            Type retour;
            type.EnsureIsInterface("type");

            if (_dic.ContainsKey(type))
                retour = _dic[type];
            else
                lock (_lock)
                    if (!_dic.ContainsKey(type))
                        _dic.Add(type, (retour = CreateWrapper(type, proxy, script)));
                    else
                        retour = _dic[type];

            return retour;
        }


        private static Type CreateWrapper(Type type, Proxy proxy, bool script)
        {


            // Initialize type
            ModuleCreator moduleCreator = GenerateWrapper(type);

            // Implemente Aop
            IBuildSave S = proxy as IBuildSave;

            if (proxy != null)            
                S.Transform(moduleCreator.TypeFinal);
           
            #region Save

            Assembly assembly = moduleCreator.Save(script);

            return assembly.GetType(moduleCreator.TypeFinal.FullName);

            #endregion



        }

        private static ModuleCreator GenerateWrapper(Type type)
        {

            #region Create Proxy Type

            string[] a = Guid.NewGuid().ToString().Split('-');
            string assemblyName = "Custom" + type.Name + a.Last();

            string filename = assemblyName + ".dll";

            var dir = new FileInfo(type.Assembly.Location).Directory.FullName;

            ModuleCreator moduleCreator = new ModuleCreator
                (
                   assemblyName,
                   filename,
                   new Version(1, 0, 0, 0),
                   Mono.Cecil.ModuleKind.Dll,
                   dir
                );

            GenericInstanceType t1 = moduleCreator.Get(typeof(Client<>)).Resolve()
                .MakeGenericInstanceType(moduleCreator.Get(type));

            string name = "Dynamic_" + t1.Name.Split('`')[0]  + "For" + type.Name;
            moduleCreator.TypeFinal = moduleCreator.OverrideType(t1, t1.Namespace, name);

            foreach (MethodDefinition item in moduleCreator.TypeFinal.Methods)
                item.IsPublic = true;

            #endregion

            var inter = moduleCreator.Get(type).Resolve();
            ImplementInterface(moduleCreator, moduleCreator.TypeFinal, inter, t1.Resolve(), inter);

            return moduleCreator;

        }


        private static void ImplementInterface(ModuleCreator moduleCreator, TypeDefinition proxy, TypeReference @interface, TypeDefinition baseProxy, TypeDefinition @interfaceFinal)
        {

            proxy.Interfaces.Add(moduleCreator.Get(@interfaceFinal));

            var interfac = proxy.Module.Import(@interface);            

            GenericInstanceType t2 = new GenericInstanceType(baseProxy);
            t2.GenericArguments.Add(@interfaceFinal);

            var c1 = baseProxy.FindMethodsByName("CreateInvoker").First();
            (c1 as MethodReference).DeclaringType = t2;

            var c2 = baseProxy.AsMethod("FinalizeInvoker");
            (c2 as MethodReference).DeclaringType = t2;

            MethodReference myMethod = proxy.Module.Import(c1);

            Dictionary<FieldDefinition, CustomAttribute> _fields = new Dictionary<FieldDefinition, CustomAttribute>();

            TypeDefinition td = @interface.GetDefinition();

            foreach (MethodDefinition m in td.Methods)
            {                

                CopyMethod cm = new CopyMethod(m);
                MethodDefinition me = cm.CloneMethod(@interface, proxy, true);
                me.IsAbstract = false;                
                me.Body = new Mono.Cecil.Cil.MethodBody(me);

                using (CilWorker gen = new CilWorker(me.Body))
                {
                    gen.Insert(OpCodes.Nop);
                    GenerateContentMethod(gen, c1, interfac, c2, interfaceFinal);
                }

            }


            foreach (var item in  td.Interfaces)            
                ImplementInterface(moduleCreator, proxy, item, baseProxy, interfaceFinal);
            

        }




        private static FieldDefinition GetField(CilWorker gen, Dictionary<FieldDefinition, CustomAttribute> _fields, CustomAttribute attribute, string name)
        {
            FieldDefinition v = null;

            foreach (FieldDefinition item in gen.Body.Method.DeclaringType.Fields)
                if (item.Name == name)
                {
                    v = item;
                    break;
                }


            if (v == null)
            {
                v = new FieldDefinition(name, Mono.Cecil.FieldAttributes.Private, gen.Body.Method.Module.Import(attribute.AttributeType));
                gen.Body.Method.DeclaringType.Fields.Add(v);
                _fields.Add(v, attribute);
            }

            return v;
        }


        private static IEnumerable<CustomAttribute> GetAttribute(Mono.Cecil.ICustomAttributeProvider p)
        {

            foreach (CustomAttribute item in p.CustomAttributes)
                if (IsValidationAttribute(item.AttributeType.Resolve()))
                    yield return item;

        }


        private static bool IsValidationAttribute(TypeDefinition item)
        {

            if (item.FullName == "System.ComponentModel.DataAnnotations.ValidationAttribute")
                return true;

            if (item.BaseType != null)
                return IsValidationAttribute(item.BaseType.Resolve());

            return false;

        }



        private static void GenerateContentMethod(CilWorker gen, MethodReference MethodCreateChannel, TypeReference @interface, MethodReference MethodRemoveChannel, TypeDefinition @interfaceFinal)
        {
          

            ModuleDefinition _module = gen.Body.Method.Module;
            // Preparation
            var variable_service = new VariableDefinition("_service", _module.Import(@interfaceFinal));
            gen.Body.Variables.Add(variable_service);
            VariableDefinition variable_retour = null;         
            if (!gen.IsVoid())
            {
                variable_retour = new VariableDefinition("var_retour", _module.Import(gen.Body.Method.ReturnType));
                gen.Body.Variables.Add(variable_retour);
            }

            // check parameters

            
            var EmitVariableService = new EmitVariableReferenceExpression(variable_service);
            var Emitvariableretour = new EmitVariableReferenceExpression(variable_retour);

            // ---------- get service -------------
            gen.Insert
            (
                new EmitAssignStatement
                (
                    EmitVariableService,
                    new EmitMethodReferenceExpression(new EmitBaseReferenceExpression(), MethodCreateChannel)
                        .Invoke()
                )
            );


            var Try = gen.CreateTryFinallyBloc();
            // ---------- Call -------------------

            EmitExpression[] args = gen.Body.Method.Parameters.ConvertInEmitExpression();

            MethodReference mm = @interface.AsMethod(gen.Body.Method);

            if (@interface.IsGenericInstance && mm.DeclaringType.HasGenericParameters)
            {

                //mm.DeclaringType = @interface;

                MethodReference mm2 = new MethodReference(mm.Name, mm.ReturnType, @interface);

                mm2.HasThis = mm.HasThis;
                mm2.ExplicitThis = mm.ExplicitThis;
                mm2.Parameters.AddRange(mm.Parameters);

                mm = mm2;
            }

            var callMethod = _module.Import(mm);

            if (gen.IsVoid())
            {
                
                Try.Insert
                (
                    new EmitMethodReferenceExpression(EmitVariableService, callMethod)
                        .Invoke(args)
                );

                Try.Insert(OpCodes.Nop);

            }
            else
            {             

                Try.Insert
                (
                    new EmitAssignStatement
                    (
                        Emitvariableretour,
                        new EmitMethodReferenceExpression(EmitVariableService, callMethod)
                            .Invoke(args)
                    )
                );


            }


        
            Try.Locate(Try.HandlerStart);

            var cast = new EmitCastExpression(new EmitTypeReference(_module.Import(typeof(ICommunicationObject))), EmitVariableService);
            Try.Insert
            (
                new EmitMethodReferenceExpression
                (
                    new EmitBaseReferenceExpression(),
                    MethodRemoveChannel
                ).Invoke(cast)
            );
            Try.Insert(OpCodes.Nop);

            // Return if need
            gen.Body.ExceptionHandlers.Add(Try.ExceptionHandler);

            Try.Locate(Try.HandlerEnd);

            if (!gen.IsVoid())
            {
                Try.Insert(new EmitReturnMethodStatement(Emitvariableretour));
            }
            else
            {
                Try.Insert(OpCodes.Ret);
            }

        }

        #endregion
        
        private static bool _script = System.Diagnostics.Debugger.IsAttached;
        public static bool Script 
        {
            get { return _script; }
            set { _script = value; }
        }

    }
}

